#!/usr/local/bin/lua
-- Script for spam filtering
-- copyright (C) 2005, 2006 Fidelis Assis <fidelis@pobox.com>
                                                 
-- spamfilter version
sf_version = "1.1.3"

require "osbf"   -- load osbf library

-- simple getopt to get command line options
function getopt(args, opt_table)
  local skip = false
  local options_found = {}
  local key
  local optind

  for i, o in ipairs(args) do
    optind = i
    if not skip then
      key, value = string.match(o, "^-%-?([^=]*)=?(.*)")

      if key then
        if key == "" then
          optind = i + 1
          break -- end of options found
        end

        if (opt_table[key] == 1) then
          if value == "" then
            options_found[key] = args[i+1]
            skip = true
          else
            options_found[key] = value
          end
        elseif (opt_table[key] == 0) then
          options_found[key] = 1
        elseif (opt_table[key] == 2) then
          if value == "" then
            options_found[key] = 1
          else
            options_found[key] = value
          end
        else
          optind = nil
          options_found = "Invalid option: " .. o
          break
        end
      else
        break -- only parameters left
      end
    else
      skip = false
    end
  end

  return optind, options_found
end

-- check if a file exists
function file_exists(file)
  local f = io.open(file, "r")
  if f then
    f:close()
    return true
  else
    return nil, "File not found"
  end
end

-- check if file exists before "doing" it
function my_dofile(file)
  local f, err_msg = loadfile(file)
  if (f) then
    f()
    return true
  else
    return false
  end
end

-- log text to filename in the log dir
function spamfilter_log(text, filename)
  local log = io.open(filename, "a+")
  if log then
    log:write(text)
    log:close()
  end
end

-- escape lua special regex characters
function escape_regchars(text)
  local s
  s = string.gsub(text, "%%", "%%%%")
  s = string.gsub(s, "([%(%)%.%+%-%*%?%[%]%^%$])", "%%%1")
  return s
end

--------------- start of program  ------------------

-- get global and user dir
local optind, options = getopt(arg, { udir = 1, gdir = 1})
if options["udir"] then
  user_osbf_dir = options["udir"]
  if string.sub(user_osbf_dir, -1) ~= "/" then
    user_osbf_dir = user_osbf_dir .. "/"
  end
else
  user_osbf_dir = "./"
end

if options["gdir"] then
  global_osbf_dir = options["gdir"]
  if string.sub(global_osbf_dir, -1) ~= "/" then
    global_osbf_dir = global_osbf_dir .. "/"
  end
else
  global_osbf_dir = "./"
end

------------------------------------------------------------------
-- path configuration. change them to use global 
-- or per user config file, databases and lists.
-- these vars must be global.

config_dir   = user_osbf_dir
database_dir = user_osbf_dir
lists_dir    = user_osbf_dir

------------------------------------------------------------------

-- read entire message into var "text"
text = io.read("*all")

if not my_dofile(config_dir .. "spamfilter_config.lua") then
  -- flush everything and return error
  io.write(text)
  io.flush()
  return 1
end


-- database definitions
osbf.cfg_dbset = {
       classes = {database_dir .. osbf.cfg_nonspam_file,
                  database_dir .. osbf.cfg_spam_file},
       ncfs    = 1, -- split "classes" in 2 sublists. "ncfs" is
                    -- the number of classes in the first sublist.
       delimiters = "" -- no extra token delimiters
    }
osbf.cfg_nonspam_index  = 1
osbf.cfg_spam_index     = 2

-- set user log dir
user_log_dir = user_osbf_dir .. osbf.cfg_log_dir .. "/"

-- read white and black lists
if not my_dofile(lists_dir .. "whitelist.lua") then
  whitelist = {}
elseif type(whitelist) ~= "table" then
  whitelist = {}
end
if not my_dofile(lists_dir .. "blacklist.lua") then
  blacklist = {}
elseif type(blacklist) ~= "table" then
  blacklist = {}
end

-- log incoming messages
if (osbf.cfg_log_incoming) then
  spamfilter_log(text, user_log_dir .. "spamfilter_inbox") 
end

-- extract header and header indexes
header_start, header, header_end = string.match(text, "^()(.-()\n)\n")
if not header then
  header_start = 0
  header_end = 0
  body_start = 0
  header = ""
else
  -- body starts at the new-line that marks the end of the header
  body_start = header_end + 1
end


-- check if we have a command in the subject
cmd, cmd_args = string.match(header .. "\n",
  "\n[Ss][Uu][Bb][Jj][Ee][Cc][Tt]%s*:[^n]-(%a+) " ..
  osbf.cfg_pwd .. "(.-)\n[^\t ]")

-- if not found in the subject, check the first lines of the body
if (not cmd) then
  cmd, cmd_args = string.match(string.sub(text, body_start, body_start + 100),
    "\n%s*(%a+) " .. osbf.cfg_pwd .. "(.-)\n")
end


-- spamfilter command?
if (cmd and not (osbf.cfg_pwd == "your_password_here") and
    (string.match(cmd_args, "^%s") or cmd_args == "")) then
  -- very probably a command was found, try to execute it.
  if my_dofile(global_osbf_dir .. "spamfilter_commands.lua") then
    valid_command, command_subj, command_body = process_command(cmd, cmd_args)
  else
    spamfilter_log("spamfilter_commands.lua not found\n", user_log_dir .. "error_log") 
  end
end

if valid_command then
   header = header .. string.format(
			"X-OSBF-Lua-Version: v%s Spamfilter v%s\n",
                        osbf._VERSION, sf_version)
  if command_subj then
    -- change subject line
    header = string.gsub(header,
    	"\n[Ss][Uu][Bb][Jj][Ee][Cc][Tt]%s*:.-\n([^ \t])", "\nSubject: " ..
	command_subj .. "\n%1", 1)
  end
  if command_body then
      -- change the body
      body = command_body
  end
  io.write(header, "\n", body)
else
  -- no command, classify the message
  -- sfid_tag will be set one of "W", "B", "E", "S", "H", "s" or "h"
  sfid_tag = ""
  subj_tag = ""

  -- check white and black lists
  pR = nil
  local f = function (i, e)
  	      return (string.find(header, e, 1, not osbf.cfg_lists_use_regex))
  	    end
  if table.foreachi(whitelist, f) then
    subj_tag = osbf.cfg_tag_ham
    sfid_tag = "W"
  elseif table.foreachi(blacklist, f) then
    subj_tag = osbf.cfg_tag_spam
    sfid_tag = "B"
  end

  pR, class_probs = osbf.classify(text, osbf.cfg_dbset, osbf.cfg_classify_flags)

  if (pR == nil) then
     classified_header = header
     subj_tag = ""
     sfid_tag = "E"
     pR = 0
     -- log error message
     spamfilter_log(class_probs, user_log_dir .. "error.log")
  elseif sfid_tag == "" then
     if (pR < -osbf.cfg_threshold) then
       subj_tag = osbf.cfg_tag_spam
       sfid_tag = "S"
     elseif (pR > osbf.cfg_threshold) then
       subj_tag = osbf.cfg_tag_ham
       sfid_tag = "H"
     elseif (pR >= 0) then
       subj_tag = osbf.cfg_tag_unsure_ham
       sfid_tag = "h"
     else
       subj_tag = osbf.cfg_tag_unsure_spam
       sfid_tag = "s"
     end
  end

  classified_header = header ..
     string.format(
       "X-OSBF-Lua-Score: %.2f/%.2f [%s] (v%s, Spamfilter v%s)\n",
       pR, osbf.cfg_min_pR_success, sfid_tag, osbf._VERSION, sf_version)

  if (osbf.cfg_tag_subject and subj_tag ~= "") then
    local i, j = string.find(classified_header,
	"\n[Ss][Uu][Bb][Jj][Ee][Cc][Tt]%s*: ?")
    
    if (i == nil) then
      -- if there's no subject, add one
      classified_header = classified_header .. "Subject: " .. subj_tag .. 
				" (no subject)\n"
    else
      while i ~= nil do
        classified_header = string.sub(classified_header, 1, j) ..
					subj_tag .. " " .. 
				       string.sub(classified_header, j+1)
        i, j = string.find(classified_header,
		 "\n[Ss][Uu][Bb][Jj][Ee][Cc][Tt]%s*: ?", j + string.len(subj_tag))
      end
    end
  end

  -- create and add a spamfilter ID as a comment to the Message-ID header
  -- use user defined right ID if available
  if (osbf.cfg_rightid) then
    rightid = "@" .. osbf.cfg_rightid
  else
    rightid = "@spamfilter.osbf.lua"
  end
  leftid = "sfid-" .. sfid_tag .. os.date("%Y%m%d-%H%M%S") .. "-"
  leftid_idx = 1
  sfid = leftid .. tostring(leftid_idx) .. rightid
  while leftid_idx < 10000 and file_exists(user_log_dir .. sfid) do
    leftid_idx = leftid_idx + 1
    sfid = leftid .. tostring(leftid_idx) .. rightid
  end
  -- check if we have a valid ID
  if leftid_idx < 10000 then
    local i, j = string.find(classified_header,
		"\n[Mm][Ee][Ss][Ss][Aa][Gg][Ee]%-[Ii][Dd]%s-:.-\n[^ \t]")
    if (i) then
      -- message-id found; add sfid to message-id
      classified_header = string.sub(classified_header, 1, j-2) ..
			" \n\t(" .. sfid .. ")" ..
			string.sub(classified_header, j-1)
    else
      -- no message-id found; create one and add the sfid
      classified_header = classified_header .. "Messaged-ID: <" .. sfid ..
			"> (" .. sfid .. ")\n"
    end
    -- save the original message for future training
    spamfilter_log(text, user_log_dir .. sfid) 
  end

  -- output the classified message
  if type(osbf.cfg_remove_body_threshold) == "number" and
     (pR < osbf.cfg_remove_body_threshold or sfid_tag == "B") then
    classified_header = string.gsub(classified_header,
             "\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]%s*:.-\n([^ \t])",
	     "\n%1")
    io.write(classified_header, "\n*** Body Removed by Spamfilter ***\n\n", 
	"Reply to yourself with the command \"recover <pwd>\" in the\n",
	"subject to recover the original message as an attachment.\n")
  else
    io.write(classified_header, string.sub(text, body_start))
  end
  io.flush()

end

